//
// Created by Administrator on 2021/11/24.
//

#include "Application.h"
#include "ShaderProgram.h"
#include "Camera.h"
#include "MeshUtility.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

class App : public Application
{
    struct PerDraw
    {
        Mat4x4f viewMatrix;
        Mat4x4f projMatrix;
        Vec3f cameraPos;
    };

    struct PointLight
    {
        Vec3f position;
        float range;
        Vec3f color;
    };

    struct Material
    {
        Vec3f specColor;
        float specStrength;
    };

    struct PerObject
    {
        Mat4x4f modelMatrix;
    };

    enum class Entity
    {
        Plane,
        Count
    };

    enum class PerUBO
    {
        PerDraw,
        PerLight,
        PerMaterial,
        Count
    };

public:
    App() : Application("Advanced Lighting", 800, 600) {}

protected:
    void OnCreate() override
    {
        SetupGL();
        SetupCamera();
        SetupPlane();
        SetupUBO();
    }

    void OnUpdate() override
    {
        UpdatePerDrawUBO();
        UpdateObjectUBO();
    }

    void OnKey(int keyCode, int action, int mods) override
    {
        if(action == GLFW_RELEASE && keyCode == GLFW_KEY_SPACE)
        {
            usePhongModel = !usePhongModel;
        }
    }

    void OnRender() override
    {
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        RenderObjectUBO();
    }

    void OnDestroy() override
    {
        glDeleteVertexArrays(int(Entity::Count), m_vao);
        glDeleteBuffers(int(Entity::Count), m_vbo);
        glDeleteBuffers(int(Entity::Count), m_ebo);
        glDeleteBuffers(int(Entity::Count), m_objectUBO);
        glDeleteTextures(int(Entity::Count), m_tex2D);
        glDeleteBuffers(int(PerUBO::Count), m_perUBO);
    }

private:
    void SetupGL()
    {
        glEnable(GL_MULTISAMPLE);
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glEnable(GL_DEPTH_TEST);

        glCreateVertexArrays(int(Entity::Count), m_vao);
        glCreateBuffers(int(Entity::Count), m_vbo);
        glCreateBuffers(int(Entity::Count), m_ebo);
        glCreateBuffers(int(Entity::Count), m_objectUBO);
        glCreateTextures(GL_TEXTURE_2D, int(Entity::Count), m_tex2D);
        glCreateBuffers(int(PerUBO::Count), m_perUBO);
    }

    void SetupCamera()
    {
        m_camera.Setup(800.0f / 600.0f, 60.0f, 0.1f, 100.0f);
        m_camera.transform().position() = Vec3f(0.0f, 3.0f, 6.0f);
        m_camera.transform().rotation() = Vec3f(-30.0f, 0.0f, 0.0f);
    }

    void SetupPlane()
    {
        Vertex* vertices;
        GLuint* indices;
        int vertexCount;
        MeshUtility::GenerateQuad(vertices, indices, vertexCount, m_indexCounts[int(Entity::Plane)]);

        m_sp[int(Entity::Plane)] = ShaderProgram("../../AdvancedLighting/shaders/plane_vs.glsl",
                                                 "../../AdvancedLighting/shaders/plane_fs.glsl");

        int width, height, channels;
        stbi_set_flip_vertically_on_load(true);
        auto* data = stbi_load("../../AdvancedLighting/textures/wood.png", &width, &height, &channels, 0);
        glBindTexture(GL_TEXTURE_2D, m_tex2D[int(Entity::Plane)]);
        if(channels == 4)
        {
            glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        }
        else
        {
            glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        }
        stbi_image_free(data);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        glBindVertexArray(m_vao[int(Entity::Plane)]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[int(Entity::Plane)]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCounts[int(Entity::Plane)] * sizeof(GLuint), indices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo[int(Entity::Plane)]);
        glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(Vec3f)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3f)));
        glEnableVertexAttribArray(2);

        delete vertices;
        delete indices;
    }

    void SetupUBO()
    {
        glNamedBufferData(m_perUBO[int(PerUBO::PerDraw)], sizeof(PerDraw), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 0, m_perUBO[int(PerUBO::PerDraw)]);
        glNamedBufferData(m_perUBO[int(PerUBO::PerLight)], sizeof(PointLight), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 1, m_perUBO[int(PerUBO::PerLight)]);
        glNamedBufferData(m_perUBO[int(PerUBO::PerMaterial)], sizeof(Material), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 2, m_perUBO[int(PerUBO::PerMaterial)]);
        glNamedBufferData(m_objectUBO[int(Entity::Plane)], sizeof(PerObject), nullptr, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 3, m_objectUBO[int(Entity::Plane)]);
    }

    void UpdatePerDrawUBO()
    {
        auto* ptr = static_cast<PerDraw*>(glMapNamedBuffer(m_perUBO[int(PerUBO::PerDraw)], GL_WRITE_ONLY));
        ptr->viewMatrix = m_camera.worldToCameraMatrix().Transposed();
        ptr->projMatrix = m_camera.projectionMatrix().Transposed();
        ptr->cameraPos = m_camera.transform().position();
        glUnmapNamedBuffer(m_perUBO[int(PerUBO::PerDraw)]);

        auto* light = static_cast<PointLight*>(glMapNamedBuffer(m_perUBO[int(PerUBO::PerLight)], GL_WRITE_ONLY));
        light->position = Vec3f(0.0f, 0.0f, 0.0f);
        light->range = 100.0f;
        light->color = Vec3f(1.0f, 1.0f, 1.0f);
        glUnmapNamedBuffer(m_perUBO[int(PerUBO::PerLight)]);

        auto* material = static_cast<Material*>(glMapNamedBuffer(m_perUBO[int(PerUBO::PerMaterial)], GL_WRITE_ONLY));
        material->specColor = Vec3f(0.3f, 0.3f, 0.3f);
        material->specStrength = 32.0f;
        glUnmapNamedBuffer(m_perUBO[int(PerUBO::PerMaterial)]);
    }

    void UpdateObjectUBO()
    {
        auto* ptr = static_cast<PerObject*>(glMapNamedBuffer(m_objectUBO[int(Entity::Plane)], GL_WRITE_ONLY));
        // S(xyz)->10 R(x):-90 T(y):-0.5
        float Rx = -90.0f * Deg2Rad;
        ptr->modelMatrix = Mat4x4f(10.0f, 0.0f,             0.0f,             0.0f,
                                   0.0f,  10.0f * cosf(Rx), 10.0f * -sinf(Rx),-0.5f,
                                   0.0f,  10.0f * sinf(Rx), 10.0f * cosf(Rx), 0.0f,
                                   0.0f,  0.0f,             0.0f,             1.0f).Transposed();
        glUnmapNamedBuffer(m_objectUBO[int(Entity::Plane)]);
    }

    void RenderObjectUBO()
    {
        m_sp[int(Entity::Plane)].Use();
        m_sp[int(Entity::Plane)].SetUniformBool("PHONG", usePhongModel);
        glBindVertexArray(m_vao[int(Entity::Plane)]);
        glDrawElements(GL_TRIANGLES, m_indexCounts[int(Entity::Plane)], GL_UNSIGNED_INT, nullptr);
    }

private:
    GLuint m_vao[int(Entity::Count)];
    GLuint m_vbo[int(Entity::Count)];
    GLuint m_ebo[int(Entity::Count)];
    GLuint m_tex2D[int(Entity::Count)];
    GLuint m_objectUBO[int(Entity::Count)];
    ShaderProgram m_sp[int(Entity::Count)];
    int m_indexCounts[int(Entity::Count)];
    GLuint m_perUBO[int(PerUBO::Count)];

    bool usePhongModel = false;

    Camera m_camera;
};

int main()
{
    App().Run();
    return 0;
}